package com.simulador.mainframe;

import java.util.ArrayList;
import java.util.PriorityQueue;
import com.simulador.mainframe.SimulatorEvent.EventType;

public class Simulator {
	private PriorityQueue<SimulatorEvent> events;
	private float clock;
	private float q;
	private float t;
	
	private float z = 1.96f;
	private float relativeError = 0.1f;
	private float correctedError = relativeError / (relativeError + 1);
	private ArrayList<Integer> samples;
	private ArrayList<Float> estimations;
	
	private long nReplications;
	private long maxTerminals;
	private long maxJobsDone;		
	
	/** State variables */
	private SimulatorEvent currentEvent;
	private boolean bCPUBusy; // Equivalent to tToBeFirstInQueue = 0 ?
	private double tToBeFirstInQueue;
	private int nClientsEnqueued;
	
	/** Statistics variables */
	private int accumulatedQueuedClients;
	private int nJobsDone;


	public Simulator() {
		samples = new ArrayList<Integer>();
		estimations = new ArrayList<Float>();
		events = new PriorityQueue<SimulatorEvent>();
		nReplications = 20;		
		maxTerminals = 150; // TODO: increment it to 100 or something high enough ?
		maxJobsDone = 150;
		q = 0.1f;
		t = 0.015f;
	}
	
	/** Restarts the values needed when simulating every replication */
	private void init(int nTerminals) {
		clock = 0;
		nJobsDone = 0;
		bCPUBusy = false;
		
		events.clear();
		tToBeFirstInQueue = 0;
		nClientsEnqueued = 0;
		accumulatedQueuedClients = 0;
		
		// Add an arrival per terminal
		for (int i = 0; i < nTerminals; ++i) {
			events.add(new SimulatorEvent(EventType.Arrivals, getNextUserThinkingTime()));
		}
	}

	/** Main simulator driver */
	public void simulate() {
		for (int i = 5; i < maxTerminals; i += 5) {
			for (int j = 0; j < nReplications; ++j) {
				mainModule(i);				
			}
			while (!isInsideConfidenceInteval()) {
				mainModule(i);
			}
			estimations.add(MathUtils.mean(samples));
			samples.clear();
		}
		
		// Simulation results
		for (Float f : estimations) {
			System.out.println(f);
		}
	}	
	
	/** Drives the simulation of one replication */
	private void mainModule(int nTerminals) {
		
		init(nTerminals);
		while (nJobsDone < maxJobsDone) {
			currentEvent = events.poll();
			clock = currentEvent.time;
			switch (currentEvent.type) {
			case Arrivals:
				arrivals();
				break;
				
			case ProcessorOutput:
				processorOutput();
				break;
			}
		}
		
		// TODO: Debugging print - remove
		System.out.println("Average clients in queue: " + accumulatedQueuedClients/nJobsDone);
		
		samples.add(accumulatedQueuedClients/nJobsDone);
	}
	
	/** Management of arrival events */	
	private void arrivals() {
		float jobTime = getNextJobDuration();
		if (!bCPUBusy) {
			if (jobTime <= q) {
				events.add(new SimulatorEvent(EventType.ProcessorOutput, clock + jobTime + t, jobTime));
			} else {
				events.add(new SimulatorEvent(EventType.ProcessorOutput, clock + q + t, jobTime));
			}
			bCPUBusy = true;
		} else {
			++nClientsEnqueued;
			if (jobTime <= q) {
				events.add(new SimulatorEvent(EventType.ProcessorOutput, clock + jobTime + t + (float)tToBeFirstInQueue, jobTime));
				tToBeFirstInQueue += jobTime + t;
			}  else {
				events.add(new SimulatorEvent(EventType.ProcessorOutput, clock + q + t + (float)tToBeFirstInQueue, jobTime));
				tToBeFirstInQueue += q + t;
			}
		}
	}
	
	/** Management of processor output events */
	private void processorOutput() {
		float jobTime = currentEvent.remainingTime;
		
		if (tToBeFirstInQueue > 0) {
			if (jobTime <= q) {
				tToBeFirstInQueue -= (jobTime + t);
			} else {
				tToBeFirstInQueue -= (q + t);
			}
		}
		
		jobTime -= q;
		if (jobTime <= 0) {
			++nJobsDone;
			accumulatedQueuedClients += nClientsEnqueued;
			events.add(new SimulatorEvent(EventType.Arrivals, clock + getNextUserThinkingTime()));
			if (nClientsEnqueued > 0) {
				--nClientsEnqueued;
			}
			if (nClientsEnqueued == 0) {
				bCPUBusy = false;
			}
		} else {
			if (jobTime <= q) {
				events.add(new SimulatorEvent(EventType.ProcessorOutput, clock + jobTime + t + (float)tToBeFirstInQueue, jobTime));
				tToBeFirstInQueue += jobTime + t; 
			} else {
				events.add(new SimulatorEvent(EventType.ProcessorOutput, clock + q + t + (float)tToBeFirstInQueue, jobTime));
				tToBeFirstInQueue += q + t;
			}
		}
	}	
	
	/** Average job's service time: 0.8s */
	private float getNextJobDuration() {
		return MathUtils.generateUniform(0.1f, 1.5f);
	}
	
	/** Average user's thinking time: 25s */
	private float getNextUserThinkingTime() {
		return MathUtils.generateUniform(15.f, 35.f);
	}

	/** Whether or not the current partial simulation is inside the confidence interval */
	private boolean isInsideConfidenceInteval() {
		float mean = MathUtils.mean(samples);
		float variance = MathUtils.variance(samples);
		double iC = z * Math.sqrt((double)variance / samples.size());

		if (mean <= 0) {
			return true;
		}
		
		if (iC / mean < correctedError) {
			return true;
		}
		
		return false;
	}

}
